﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Algoritmia.Problems.Matrix
{
    public class SquareDoubleMatrix
    {
        private double[][] _matrix;
        private readonly int _n;

        public SquareDoubleMatrix(double[][] matrix = null, int n = 0)
        {
            if ( matrix != null)
            {
                if ( matrix.Any( row => row.Length != matrix.Length))
                {
                    throw new ArgumentException("matrix parameter must be a square matrix");
                }
                _matrix = matrix;
                _n = matrix.Length;
            }
            else
            {
                _n = n;
                if ( _n > 0)
                {
                    _matrix = new double[_n][];
                    for(int i = 0; i < _n; i++)
                    {
                        _matrix[i] = new double[_n];
                    }
                }
            }
        }

        public int Length
        {
            get { return _n; }
        }

        public double[] this[int index]
        {
            get
            {
                if ( index >= 0 && index < _n)
                {
                    return _matrix[index];
                }
                throw new ArgumentOutOfRangeException("index");
            }
        }

        private IEnumerable<SquareDoubleMatrix> Split()
        {
            var mid = _n/2;
            yield return new SquareDoubleMatrix((from i in Enumerable.Range(0, mid)
                                                  select (from j in Enumerable.Range(0, mid)
                                                          select _matrix[i][j]).ToArray()).ToArray());
            yield return new SquareDoubleMatrix((from i in Enumerable.Range(0, mid)
                                                  select (from j in Enumerable.Range(mid, _n-mid)
                                                          select _matrix[i][j]).ToArray()).ToArray());
            yield return new SquareDoubleMatrix((from i in Enumerable.Range(mid, _n-mid)
                                                  select (from j in Enumerable.Range(0, mid)
                                                          select _matrix[i][j]).ToArray()).ToArray());
            yield return new SquareDoubleMatrix((from i in Enumerable.Range(mid, _n-mid)
                                                  select (from j in Enumerable.Range(mid, _n-mid)
                                                          select _matrix[i][j]).ToArray()).ToArray());
        }

        private void Join(SquareDoubleMatrix a, SquareDoubleMatrix b, 
                                         SquareDoubleMatrix c, SquareDoubleMatrix d)
        {
            var mid = _n/2;
            for(int i = 0; i < mid; i++)
            {
                for(int j = 0; j < mid; j++)
                {
                    this[i][j] = a[i][j];
                }
                for(int j = mid; j < _n; j++)
                {
                    this[i][j] = b[i][j - mid];
                }
            }
            for(int i = mid; i <_n ; i++)
            {
                for (int j = 0; j < mid; j++)
                {
                    this[i][j] = c[i-mid][j];
                }
                for (int j = mid; j < _n; j++)
                {
                    this[i][j] = d[i-mid][j - mid];
                }
            }
        }

        public SquareDoubleMatrix Add(SquareDoubleMatrix other)
        {
            return new SquareDoubleMatrix((from i in Enumerable.Range(0,_n)
                                           select (from j in Enumerable.Range(0,_n)
                                                   select _matrix[i][j]+other[i][j]).ToArray()).ToArray()
                                            );
        }

        public SquareDoubleMatrix Substract(SquareDoubleMatrix other)
        {
            return new SquareDoubleMatrix((from i in Enumerable.Range(0, _n)
                                            select (from j in Enumerable.Range(0, _n)
                                                    select _matrix[i][j] - other[i][j]).ToArray()).ToArray()
                                            );
        }

        public SquareDoubleMatrix StrassenProduct(SquareDoubleMatrix other)
        {
            if (_n != other._n)
                throw new ArgumentException("the matrix must have de same lenght as this", "other");
            if ( _n == 1)
            {
                return new SquareDoubleMatrix(new [] { new [] {other[0][0]*this[0][0]} });
            }
            var firstSplit = Split().ToList();
            var a = firstSplit[0];
            var b = firstSplit[1];
            var c = firstSplit[2];
            var d = firstSplit[3];

            var secondSplit = other.Split().ToList();
            var e = secondSplit[0];
            var f = secondSplit[1];
            var g = secondSplit[2];
            var h = secondSplit[3];
            var P1 = a * (f - h);
            var P2 = (a + b) * h;
            var P3 = (c + d) * e;
            var P4 = d * (g - e);
            var P5 = (a + d) * (e + h);
            var P6 = (b - d) * (g + h);
            var P7 = (a - c) * (e + f);

            var r = P5 + P4 - P2 + P6;
            var s = P1 + P2;
            var t = P3 + P4;
            var u = P5 + P1 - P3 - P7;

            var result = new SquareDoubleMatrix(n: _n);
            result.Join(r,s,t,u);
            return result;
        }

        public static SquareDoubleMatrix operator + (SquareDoubleMatrix a, SquareDoubleMatrix b)
        {
            return a.Add(b);
        }

        public static SquareDoubleMatrix operator -(SquareDoubleMatrix a, SquareDoubleMatrix b)
        {
            return a.Substract(b);
        }

        public static SquareDoubleMatrix operator *(SquareDoubleMatrix a, SquareDoubleMatrix b)
        {
            return a.StrassenProduct(b);
        }

        public override string ToString()
        {
            return string.Join(",\r\n", from row in _matrix
                                        select "[" + string.Join(",", row) + "]");
        }
    }
}
